/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Collections.Generic;
using System.Configuration.Provider;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

namespace DaveSexton.DocProject.DocSites
{
	/// <remarks>
	/// This class is not thread-safe.  Do not call into an instance of this class directly from web pages.
	/// Instead call the methods on the <see cref="DocSiteSearch"/> class to ensure thread-safety among web requests.
	/// </remarks>
	public sealed class DocSiteMemorySearchProvider : DocSiteSearchProvider
	{
		#region Public Properties
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate")]
		public static readonly string DefaultSplitQueryCharacters = ":;+*\\/|`~,.\"";
		public static readonly QueryOperator DefaultQueryOperator = '&';

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1043:UseIntegralOrStringArgumentForIndexers")]
		public override IEnumerable<IndexEntry> this[params string[] keywords]
		{
			get
			{
				IDictionary<string, IndexEntry> all = null;

				foreach (string keyword in keywords)
				{
					IDictionary<string, IndexEntry> entries = null;

					if (index.TryGetValue(keyword, out entries) && entries != null)
					{
						if (all == null)
							all = entries;
						else
							all = ListHelper.Union(all, entries, StringComparer.OrdinalIgnoreCase);
					}
				}

				if (all != null)
					foreach (IndexEntry entry in all.Values)
						yield return entry;
			}
		}

		public override int KeywordCount
		{
			get
			{
				return index.Count;
			}
		}

		public override int DocumentCount
		{
			get
			{
				return allEntries.Count;
			}
		}

		public override IEnumerable<KeywordEntry> Keywords
		{
			get
			{
				foreach (KeyValuePair<string, IDictionary<string, IndexEntry>> pair in index)
					yield return new KeywordEntry(pair.Key, pair.Value.Values);
			}
		}

		public override IEnumerable<QueryOperator> UnaryQueryOperators
		{
			get
			{
				yield return '-';
			}
		}

		public override string SplitQueryCharacters
		{
			get
			{
				return splitQueryCharacters;
			}
		}

		public override QueryOperator DefaultOperator
		{
			get
			{
				return defaultOperator;
			}
		}

		public override bool OptimizeQueries
		{
			get
			{
				return true;
			}
		}
		#endregion

		#region Private / Protected
		private QueryOperator defaultOperator;
		private string splitQueryCharacters;
		private readonly IDictionary<string, IDictionary<string, IndexEntry>> index = new Dictionary<string, IDictionary<string, IndexEntry>>(512, StringComparer.CurrentCultureIgnoreCase);
		private readonly IDictionary<string, IndexEntry> allEntries = new Dictionary<string, IndexEntry>(1024, StringComparer.OrdinalIgnoreCase);
		private readonly int nonMatchedKeywordWeight = 1, combinedMatchWeight = 10;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="DocSiteMemorySearchProvider" /> class.
		/// </summary>
		public DocSiteMemorySearchProvider()
		{
		}
		#endregion

		#region Methods
		public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
		{
			if (config == null)
				throw new ArgumentNullException("config");

			if (string.IsNullOrEmpty(name))
				name = "DocSiteMemorySearchProvider";

			if (string.IsNullOrEmpty(config["description"]))
			{
				config.Remove("description");
				config.Add("description", "DocSite In-Memory Index Search Provider");
			}

			base.Initialize(name, config);

			splitQueryCharacters = config["splitQueryCharacters"];

			if (splitQueryCharacters == null)
				splitQueryCharacters = DefaultSplitQueryCharacters;
			else
				config.Remove("splitQueryCharacters");

			string c = config["defaultOperator"];

			if (string.IsNullOrEmpty(c))
				defaultOperator = DefaultQueryOperator;
			else
				defaultOperator = c[0];

			config.Remove("defaultOperator");

			// Throw an exception if unrecognized attributes remain
			if (config.Count > 0)
			{
				string attr = config.GetKey(0);

				if (!string.IsNullOrEmpty(attr))
					throw new ProviderException(string.Format(System.Globalization.CultureInfo.CurrentCulture, Resources.Errors.UnrecognizedAttribute, attr));
			}
		}

		public override void ClearIndex()
		{
			index.Clear();
			allEntries.Clear();
		}

		public override void AddKeyword(string word, IndexEntry entry)
		{
			string file = entry.File;

			if (index.ContainsKey(word))
			{
				IDictionary<string, IndexEntry> entries = index[word];

				if (entries.ContainsKey(file))
					entries[file] = entries[file].IncrementOccurrencesAndWeight(1, entry.Weight);
				else
				{
					entries.Add(file, entry);
					allEntries[file] = entry;
				}
			}
			else
			{
				IDictionary<string, IndexEntry> entries = new Dictionary<string, IndexEntry>(256);
				entries.Add(file, entry);

				index[word] = entries;
				allEntries[file] = entry;
			}
		}

		public override bool HasKeyword(string word)
		{
			return index.ContainsKey(word);
		}

		public override QueryOperator GetQueryOperator(string term)
		{
			if (term == "|" || string.Equals(term, "OR", StringComparison.OrdinalIgnoreCase))
				return '|';
			else if (term == "&" || string.Equals(term, "AND", StringComparison.OrdinalIgnoreCase))
				return '&';
			else
				return QueryOperator.None;
		}

		private List<IndexMatch> EvaluateOperation(UnaryQueryOperation operation, IDictionary<QueryExpression, List<IndexMatch>> subexpressions)
		{
			switch (operation.Operator)
			{
				case '-':
					TermQueryExpression termExpression = operation.Operand as TermQueryExpression;

					List<IndexMatch> positiveMatches;

					if (termExpression == null)
						// operand is a complex subexpression that has already been evaluated
						positiveMatches = subexpressions[operation.Operand];
					else
					{
						string term = termExpression.Term;

						positiveMatches = new List<IndexMatch>(128);

						FindAllMatches(term, positiveMatches);
					}

					List<IndexMatch> matches = new List<IndexMatch>(128);

					foreach (IndexEntry entry in allEntries.Values)
					{
						IndexMatch match = new IndexMatch(entry, nonMatchedKeywordWeight);

						int indexOfMatch = positiveMatches.IndexOf(match);

						if (indexOfMatch < 0)
							matches.Add(match);
					}

					return matches;
				default:
					throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture, 
						Resources.Errors.UnaryQueryOperatorNotSuppored, operation.Operator));
			}
		}

		private List<IndexMatch> EvaluateOperation(MultiQueryOperation operation, IDictionary<QueryExpression, List<IndexMatch>> subexpressions)
		{
			List<string> subterms = new List<string>(operation.OperandCount);
			Dictionary<QueryExpression, List<IndexMatch>> suboperations = new Dictionary<QueryExpression, List<IndexMatch>>();

			// segregate terms and operations
			foreach (QueryExpression expr in operation.Operands)
			{
				TermQueryExpression term = expr as TermQueryExpression;

				if (term != null)
					subterms.Add(term.Term);
				else
				{
					QueryOperation op = expr as QueryOperation;

					if (op != null)
						suboperations.Add(op, subexpressions[op]);
				}
			}

			switch (operation.Operator)
			{
				case '&':
					return EvaluateAND(subterms, suboperations);
				case '|':
					return EvaluateOR(subterms, suboperations);
				default:
					throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
						Resources.Errors.QueryOperatorNotSuppored, operation.Operator));
			}
		}

		private List<IndexMatch> EvaluateOR(List<string> terms, IDictionary<QueryExpression, List<IndexMatch>> operations)
		{
			List<IndexMatch> matches = new List<IndexMatch>(128);

			// combine subexpression matches
			foreach (KeyValuePair<QueryExpression, List<IndexMatch>> op in operations)
			{
				IList<IndexMatch> opMatches = op.Value;

				if (matches.Count == 0)
					matches.AddRange(opMatches);
				else
				{
					matches = new List<IndexMatch>(ListHelper.Combine(matches, opMatches, 
						delegate(IndexMatch match, bool isDuplicate)
						{
							if (isDuplicate)
								match.AddRank(combinedMatchWeight);
						}));
				}
			}

			if (terms.Count > 0)
				EvaluateTerms(terms, true, ref matches);

			return matches;
		}

		private List<IndexMatch> EvaluateAND(List<string> terms, IDictionary<QueryExpression, List<IndexMatch>> operations)
		{
			List<IndexMatch> matches = new List<IndexMatch>(128);

			// union subexpression matches
			foreach (KeyValuePair<QueryExpression, List<IndexMatch>> op in operations)
			{
				List<IndexMatch> opMatches = op.Value;

				if (matches.Count == 0)
					matches.AddRange(opMatches);
				else
				{
					matches = new List<IndexMatch>(ListHelper.Union(matches, opMatches, 
						delegate(IndexMatch match)
						{
							match.AddRank(combinedMatchWeight);
						}));
				}
			}

			if (terms.Count > 0 && (operations.Count == 0 || matches.Count > 0))
				EvaluateTerms(terms, false, ref matches);

			return matches;
		}

		private void EvaluateTerms(IEnumerable<string> terms, bool combineAll, ref List<IndexMatch> matches)
		{
			List<List<IndexMatch>> segregatedMatches = (!combineAll) ? new List<List<IndexMatch>>(8) : null;

			foreach (string term in terms)
			{
				List<IndexMatch> currentMatches;

				if (combineAll)
					currentMatches = matches;
				else
				{
					currentMatches = new List<IndexMatch>(128);
					segregatedMatches.Add(currentMatches);
				}

				FindExactMatches(term, currentMatches);
				FindPartialMatches(term, currentMatches);
			}

			if (segregatedMatches != null)
				UnionAll(segregatedMatches, ref matches);
		}

		private void FindExactMatches(string term, List<IndexMatch> matches)
		{
			foreach (IndexEntry entry in this[term])
			{
				int rank = entry.Occurrences + entry.Weight + DocSiteSearch.GetAdditionalRankForTitleMatch(entry, term);

				IndexMatch match = new IndexMatch(entry, rank);

				int indexOfMatch = matches.IndexOf(match);

				if (indexOfMatch > -1)
					matches[indexOfMatch] = matches[indexOfMatch].AddRank(rank);
				else
					matches.Add(match);
			}
		}

		private void FindPartialMatches(string term, List<IndexMatch> matches)
		{
			foreach (KeyValuePair<string, IDictionary<string, IndexEntry>> pair in index)
			{
				string key = pair.Key;
				int keyLength = key.Length;
				int termLength = term.Length;

				if (keyLength > termLength && key.IndexOf(term, StringComparison.CurrentCultureIgnoreCase) > -1)
				{
					foreach (IndexEntry entry in pair.Value.Values)
					{
						int rank = ((int) ((entry.Occurrences + entry.Weight) * (termLength / (float) keyLength)))
							+ DocSiteSearch.GetAdditionalRankForTitleMatch(entry, term);

						IndexMatch match = new IndexMatch(entry, rank);

						int indexOfMatch = matches.IndexOf(match);

						if (indexOfMatch > -1)
							matches[indexOfMatch] = matches[indexOfMatch].AddRank(rank);
						else
							matches.Add(match);
					}
				}
			}
		}

		private void FindAllMatches(string term, List<IndexMatch> matches)
		{
			foreach (KeyValuePair<string, IDictionary<string, IndexEntry>> pair in index)
			{
				string key = pair.Key;
				int keyLength = key.Length;
				int termLength = term.Length;

				if (keyLength >= termLength && key.IndexOf(term, StringComparison.CurrentCultureIgnoreCase) > -1)
				{
					foreach (IndexEntry entry in pair.Value.Values)
					{
						IndexMatch match = new IndexMatch(entry, 0);

						int indexOfMatch = matches.IndexOf(match);

						if (indexOfMatch < 0)
							matches.Add(match);
					}
				}
			}
		}

		private void UnionAll(List<List<IndexMatch>> newMatches, ref List<IndexMatch> existingMatches)
		{
			bool combine = existingMatches.Count == 0;

			foreach (List<IndexMatch> matches in newMatches)
			{
				if (combine)
				{
					existingMatches = new List<IndexMatch>(ListHelper.Combine(matches, existingMatches, delegate(IndexMatch match, bool isDuplicate)
					{
						if (isDuplicate)
							match.AddRank(combinedMatchWeight);
					}));
				}
				else
				{
					existingMatches = new List<IndexMatch>(ListHelper.Union(matches, existingMatches, delegate(IndexMatch match)
					{
						match.AddRank(combinedMatchWeight);
					}));
				}

				combine = false;
			}
		}

		public override IEnumerable<IndexMatch> Search(QueryExpression query)
		{
			if (query == null)
				throw new ArgumentNullException("query");

			List<IndexMatch> matches;

			TermQueryExpression termExpression = query as TermQueryExpression;

			if (termExpression != null)
			{
				matches = new List<IndexMatch>(128);

				EvaluateTerms(new string[] { termExpression.Term }, true, ref matches);
			}
			else
			{
				// The main dictionary's key is a parent expresssion, and the key for its value is a subxpression, and its related value is the 
				// result of the subexpressions' evaluation (a list of matches).
				Dictionary<QueryExpression, Dictionary<QueryExpression, List<IndexMatch>>> parentExpressions =
					new Dictionary<QueryExpression, Dictionary<QueryExpression, List<IndexMatch>>>();

				TermQueryExpression topMostParentPlaceholder = new TermQueryExpression("");

				query.Evaluate(delegate(QueryExpression expr)
				// Evaluate expressions from left to right.  A parent expression is not evaluated until all of its subexpressions have been 
				// evaluated first.  Evaluation of term expressions is deferred until the parent is evaluated.  (Note that optimizations may
				// change the order of subexpressions from how they were originally entered in the text query.)
				{
					Dictionary<QueryExpression, List<IndexMatch>> subexpressions = (parentExpressions.ContainsKey(expr.Parent ?? topMostParentPlaceholder))
						? parentExpressions[expr.Parent ?? topMostParentPlaceholder]
						: parentExpressions[expr.Parent ?? topMostParentPlaceholder] = new Dictionary<QueryExpression, List<IndexMatch>>();

					TermQueryExpression term = expr as TermQueryExpression;

					if (term != null)
					{
						// store terms as subexpressions that haven't been evaluated yet (the list value is null)
						subexpressions.Add(term, null);
					}
					else
					// immediately evaluate non-term expressions and their subexpressions
					{
						List<IndexMatch> result = null;

						MultiQueryOperation operation = expr as MultiQueryOperation;

						if (operation != null)
						{
							result = EvaluateOperation(operation, parentExpressions[operation]);

							subexpressions.Add(operation, result);
						}
						else
						{
							UnaryQueryOperation unary = expr as UnaryQueryOperation;

							if (unary != null)
							{
								result = EvaluateOperation(unary, parentExpressions[unary]);

								subexpressions.Add(unary, result);
							}
						}

						parentExpressions.Remove(expr);
					}
				});

				matches = parentExpressions[topMostParentPlaceholder][query];
			}

			matches.Sort(IndexMatchSorter.Default);

			return matches;
		}
		#endregion
	}
}
